Appearance
[English version](/posts/c教程/en-US/08-Debugging Testing Profiling/078-Crash Analysis and Core Dumps)
即使进行了仔细的编码和测试,程序也会崩溃。在 C 语言中,崩溃是系统表达“你触及了不该触及的东西”的方式。好消息是,您可以使用核心转储(故障时程序内存的快照)科学地分析崩溃。
学习如何阅读它们是每个系统程序员的一项基本技能。
步骤 1.什么是核心转储?
核心转储是一个捕获程序崩溃时的状态(堆栈、寄存器、内存)的文件。您可以稍后使用 gdb 之类的调试器检查它,看看出了什么问题。
生成核心转储的常见崩溃信号:
- SIGSEGV – 无效的内存访问(分段错误)
- SIGABRT – 断言失败
- SIGFPE – 算术错误(如被零除)
- SIGILL – 非法指令
步骤 2. 启用核心转储
默认情况下,现代系统会限制或禁用它们。使用以下方法启用它们:
ulimit -c unlimited查看:
ulimit -a | grep core现在,当你的程序崩溃时,一个名为core或者core. 将会出现。
步骤 3. 一个崩溃的例子
让我们做一个简单的崩溃:
#include <stdio.h>
int main(void) {
int *p = NULL;
*p = 42; // crash: dereferencing null pointer
return 0;
}编译并运行:
gcc crash.c -g -o crash
./crash输出:
Segmentation fault (core dumped)一个core文件现已在您的目录中创建。
步骤 4. 在 gdb 中打开核心转储
使用 gdb 检查发生了什么:
gdb ./crash core你会看到:
Core was generated by `./crash'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0 0x0000000000401136 in main () at crash.c:5
5 *p = 42;这会告诉您程序崩溃的确切行。
步骤 5. 检查变量和堆栈
在 gdb 中:
(gdb) bt显示回溯,即崩溃时的调用堆栈。
#0 main () at crash.c:5您可以检查变量:
(gdb) info locals
(gdb) print p输出:
$1 = (int *) 0x0步骤 6. 更复杂的示例
#include <stdio.h>
void f3(int *p) {
*p = 7; // likely crash here
}
void f2(int *p) {
f3(p);
}
void f1(void) {
int *x = NULL;
f2(x);
}
int main(void) {
f1();
}运行它,使其崩溃,然后:
gdb ./crash core内部gdb:
(gdb) bt
#0 f3 (p=0x0) at crash.c:4
#1 f2 (p=0x0) at crash.c:8
#2 f1 () at crash.c:12
#3 main () at crash.c:16您可以看到导致空取消引用的整个调用链。
步骤 7. 调试优化构建
当你编译时-O2或者-O3,编译器可能会内联或重新排序代码,从而使调试变得更加困难。对于调试,请始终使用:
gcc -g -O0 crash.c -o crash这-gflag 保存符号信息(文件名、行号)。没有它,除了地址之外,gdb 无法告诉您更多信息。
步骤 8. 控制核心转储位置
更改转储的存储位置:
sudo sysctl -w kernel.core_pattern=/tmp/core.%e.%p这个例子将它们保存在/tmp包含程序名称和进程 ID:
/tmp/core.crash.1234步骤 9. 崩溃分析工作流程
启用转储:ulimit -c unlimited
运行程序直到崩溃
找到核心文件
用gdb分析:
-bt用于堆栈跟踪 -info locals对于本地变量 -frame 检查每个堆栈帧 -list查看附近的代码
这可以让您全面了解失败之前发生的情况。
步骤 10. 小代码:断言失败分析
#include <assert.h>
#include <stdio.h>
int main(void) {
int x = 5;
assert(x == 10); // fails
printf("Done\n");
}编译并运行:
gcc -g assert_fail.c -o assert_fail
ulimit -c unlimited
./assert_fail输出:
assert_fail: assert_fail.c:5: main: Assertion `x == 10' failed.
Aborted (core dumped)现在检查:
gdb ./assert_fail core
(gdb) bt输出:
#0 0x00007f0a9a4b9187 in raise () from /lib64/libc.so.6
#1 0x00007f0a9a4c53e8 in abort () from /lib64/libc.so.6
#2 0x00007f0a9a4b0246 in __assert_fail_base () from /lib64/libc.so.6
#3 0x00007f0a9a4b02f2 in __assert_fail () from /lib64/libc.so.6
#4 0x0000000000401136 in main () at assert_fail.c:5您可以准确跟踪断言如何导致中止信号。
为什么它很重要
崩溃分析将混乱变得清晰。您可以:
- 查看哪一行导致了崩溃。
- 检查此时的所有变量。
- 快速重现并修复错误。
每个认真的 C 程序员都必须掌握这一点,这是系统工程师调试从用户工具到内核的所有内容的方法。
自己尝试一下
- 编写一个取消引用空指针的小程序。
- 启用核心转储(
ulimit -c unlimited). - 崩溃,然后在 gdb 中分析转储。
- 再添加一层函数并检查调用链。
- 尝试
SIGFPE(除以零)并查看核心转储有何不同。
接下来,您将为 C 项目、习惯和原则构建一个代码审查清单,以帮助防止这些崩溃发生。
